Passed
Branch wavefile-reader (b7dc7a)
by Rafael S.
02:25
created

WaveFileReader.readBextChunk_   A

Complexity

Conditions 2

Size

Total Lines 27
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 27
rs 9.304
c 0
b 0
f 0
cc 2
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileReader class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import RIFFFile from './riff-file';
33
import {unpackString, unpack} from 'byte-data';
34
35
/**
36
 * A class to read wav files.
37
 */
38
export default class WaveFileReader extends RIFFFile {
39
40
  /**
41
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
42
   * @throws {Error} If container is not RIFF, RIFX or RF64.
43
   * @throws {Error} If format is not WAVE.
44
   * @throws {Error} If no 'fmt ' chunk is found.
45
   * @throws {Error} If no 'data' chunk is found.
46
   */
47
  constructor(wavBuffer=null) {
48
    super();
49
    // Include 'RF64' as a supported container format
50
    this.supported_containers.push('RF64');
51
    /**
52
     * The data of the 'fmt' chunk.
53
     * @type {!Object<string, *>}
54
     */
55
    this.fmt = {
56
      /** @type {string} */
57
      chunkId: '',
58
      /** @type {number} */
59
      chunkSize: 0,
60
      /** @type {number} */
61
      audioFormat: 0,
62
      /** @type {number} */
63
      numChannels: 0,
64
      /** @type {number} */
65
      sampleRate: 0,
66
      /** @type {number} */
67
      byteRate: 0,
68
      /** @type {number} */
69
      blockAlign: 0,
70
      /** @type {number} */
71
      bitsPerSample: 0,
72
      /** @type {number} */
73
      cbSize: 0,
74
      /** @type {number} */
75
      validBitsPerSample: 0,
76
      /** @type {number} */
77
      dwChannelMask: 0,
78
      /**
79
       * 4 32-bit values representing a 128-bit ID
80
       * @type {!Array<number>}
81
       */
82
      subformat: []
83
    };
84
    /**
85
     * The data of the 'fact' chunk.
86
     * @type {!Object<string, *>}
87
     */
88
    this.fact = {
89
      /** @type {string} */
90
      chunkId: '',
91
      /** @type {number} */
92
      chunkSize: 0,
93
      /** @type {number} */
94
      dwSampleLength: 0
95
    };
96
    /**
97
     * The data of the 'cue ' chunk.
98
     * @type {!Object<string, *>}
99
     */
100
    this.cue = {
101
      /** @type {string} */
102
      chunkId: '',
103
      /** @type {number} */
104
      chunkSize: 0,
105
      /** @type {number} */
106
      dwCuePoints: 0,
107
      /** @type {!Array<!Object>} */
108
      points: [],
109
    };
110
    /**
111
     * The data of the 'smpl' chunk.
112
     * @type {!Object<string, *>}
113
     */
114
    this.smpl = {
115
      /** @type {string} */
116
      chunkId: '',
117
      /** @type {number} */
118
      chunkSize: 0,
119
      /** @type {number} */
120
      dwManufacturer: 0,
121
      /** @type {number} */
122
      dwProduct: 0,
123
      /** @type {number} */
124
      dwSamplePeriod: 0,
125
      /** @type {number} */
126
      dwMIDIUnityNote: 0,
127
      /** @type {number} */
128
      dwMIDIPitchFraction: 0,
129
      /** @type {number} */
130
      dwSMPTEFormat: 0,
131
      /** @type {number} */
132
      dwSMPTEOffset: 0,
133
      /** @type {number} */
134
      dwNumSampleLoops: 0,
135
      /** @type {number} */
136
      dwSamplerData: 0,
137
      /** @type {!Array<!Object>} */
138
      loops: []
139
    };
140
    /**
141
     * The data of the 'bext' chunk.
142
     * @type {!Object<string, *>}
143
     */
144
    this.bext = {
145
      /** @type {string} */
146
      chunkId: '',
147
      /** @type {number} */
148
      chunkSize: 0,
149
      /** @type {string} */
150
      description: '', //256
151
      /** @type {string} */
152
      originator: '', //32
153
      /** @type {string} */
154
      originatorReference: '', //32
155
      /** @type {string} */
156
      originationDate: '', //10
157
      /** @type {string} */
158
      originationTime: '', //8
159
      /**
160
       * 2 32-bit values, timeReference high and low
161
       * @type {!Array<number>}
162
       */
163
      timeReference: [0, 0],
164
      /** @type {number} */
165
      version: 0, //WORD
166
      /** @type {string} */
167
      UMID: '', // 64 chars
168
      /** @type {number} */
169
      loudnessValue: 0, //WORD
170
      /** @type {number} */
171
      loudnessRange: 0, //WORD
172
      /** @type {number} */
173
      maxTruePeakLevel: 0, //WORD
174
      /** @type {number} */
175
      maxMomentaryLoudness: 0, //WORD
176
      /** @type {number} */
177
      maxShortTermLoudness: 0, //WORD
178
      /** @type {string} */
179
      reserved: '', //180
180
      /** @type {string} */
181
      codingHistory: '' // string, unlimited
182
    };
183
    /**
184
     * The data of the 'ds64' chunk.
185
     * Used only with RF64 files.
186
     * @type {!Object<string, *>}
187
     */
188
    this.ds64 = {
189
      /** @type {string} */
190
      chunkId: '',
191
      /** @type {number} */
192
      chunkSize: 0,
193
      /** @type {number} */
194
      riffSizeHigh: 0, // DWORD
195
      /** @type {number} */
196
      riffSizeLow: 0, // DWORD
197
      /** @type {number} */
198
      dataSizeHigh: 0, // DWORD
199
      /** @type {number} */
200
      dataSizeLow: 0, // DWORD
201
      /** @type {number} */
202
      originationTime: 0, // DWORD
203
      /** @type {number} */
204
      sampleCountHigh: 0, // DWORD
205
      /** @type {number} */
206
      sampleCountLow: 0 // DWORD
207
      /** @type {number} */
208
      //'tableLength': 0, // DWORD
209
      /** @type {!Array<number>} */
210
      //'table': []
211
    };
212
    /**
213
     * The data of the 'data' chunk.
214
     * @type {!Object<string, *>}
215
     */
216
    this.data = {
217
      /** @type {string} */
218
      chunkId: '',
219
      /** @type {number} */
220
      chunkSize: 0,
221
      /** @type {!Uint8Array} */
222
      samples: new Uint8Array(0)
223
    };
224
    /**
225
     * The data of the 'LIST' chunks.
226
     * Each item in this list look like this:
227
     *  {
228
     *      chunkId: '',
229
     *      chunkSize: 0,
230
     *      format: '',
231
     *      subChunks: []
232
     *   }
233
     * @type {!Array<!Object>}
234
     */
235
    this.LIST = [];
236
    /**
237
     * The data of the 'junk' chunk.
238
     * @type {!Object<string, *>}
239
     */
240
    this.junk = {
241
      /** @type {string} */
242
      chunkId: '',
243
      /** @type {number} */
244
      chunkSize: 0,
245
      /** @type {!Array<number>} */
246
      chunkData: []
247
    };
248
    /**
249
     * @type {!Object}
250
     * @protected
251
     */
252
    this.uInt16 = {bits: 16, be: false};
253
    // Load a file from the buffer if one was passed
254
    // when creating the object
255
    if (wavBuffer) {
256
      this.fromBuffer(wavBuffer);
257
    }
258
  }
259
260
  /**
261
   * Set up the WaveFileReader object from a byte buffer.
262
   * @param {!Uint8Array} wavBuffer The buffer.
263
   * @param {boolean=} samples True if the samples should be loaded.
264
   * @throws {Error} If container is not RIFF, RIFX or RF64.
265
   * @throws {Error} If format is not WAVE.
266
   * @throws {Error} If no 'fmt ' chunk is found.
267
   * @throws {Error} If no 'data' chunk is found.
268
   */
269
  fromBuffer(wavBuffer, samples=true) {
270
    this.clearHeader();
271
    this.setSignature(wavBuffer);
272
    this.uInt16.be = this.uInt32.be;
273
    if (this.format != 'WAVE') {
274
      throw Error('Could not find the "WAVE" format identifier');
275
    }
276
    this.readDs64Chunk_(wavBuffer);
277
    this.readFmtChunk_(wavBuffer);
278
    this.readFactChunk_(wavBuffer);
279
    this.readBextChunk_(wavBuffer);
280
    this.readCueChunk_(wavBuffer);
281
    this.readSmplChunk_(wavBuffer);
282
    this.readDataChunk_(wavBuffer, samples);
283
    this.readJunkChunk_(wavBuffer);
284
    this.readLISTChunk_(wavBuffer);
285
  }
286
287
  /**
288
   * Reset some attributes of the object.
289
   * @protected
290
   * @ignore
291
   */
292
  clearHeader() {
293
    this.fmt.cbSize = 0;
294
    this.fmt.validBitsPerSample = 0;
295
    this.fact.chunkId = '';
296
    this.ds64.chunkId = '';
297
  }
298
299
  /**
300
   * Read the 'fmt ' chunk of a wave file.
301
   * @param {!Uint8Array} buffer The wav file buffer.
302
   * @throws {Error} If no 'fmt ' chunk is found.
303
   * @private
304
   */
305
  readFmtChunk_(buffer) {
306
    /** @type {?Object} */
307
    let chunk = this.findChunk('fmt ');
308
    if (chunk) {
309
      this.head = chunk.chunkData.start;
310
      this.fmt.chunkId = chunk.chunkId;
311
      this.fmt.chunkSize = chunk.chunkSize;
312
      this.fmt.audioFormat = this.readUInt16(buffer);
313
      this.fmt.numChannels = this.readUInt16(buffer);
314
      this.fmt.sampleRate = this.readUInt32(buffer);
315
      this.fmt.byteRate = this.readUInt32(buffer);
316
      this.fmt.blockAlign = this.readUInt16(buffer);
317
      this.fmt.bitsPerSample = this.readUInt16(buffer);
318
      this.readFmtExtension_(buffer);
319
    } else {
320
      throw Error('Could not find the "fmt " chunk');
321
    }
322
  }
323
324
  /**
325
   * Read the 'fmt ' chunk extension.
326
   * @param {!Uint8Array} buffer The wav file buffer.
327
   * @private
328
   */
329
  readFmtExtension_(buffer) {
330
    if (this.fmt.chunkSize > 16) {
331
      this.fmt.cbSize = this.readUInt16(buffer);
332
      if (this.fmt.chunkSize > 18) {
333
        this.fmt.validBitsPerSample = this.readUInt16(buffer);
334
        if (this.fmt.chunkSize > 20) {
335
          this.fmt.dwChannelMask = this.readUInt32(buffer);
336
          this.fmt.subformat = [
337
            this.readUInt32(buffer),
338
            this.readUInt32(buffer),
339
            this.readUInt32(buffer),
340
            this.readUInt32(buffer)];
341
        }
342
      }
343
    }
344
  }
345
346
  /**
347
   * Read the 'fact' chunk of a wav file.
348
   * @param {!Uint8Array} buffer The wav file buffer.
349
   * @private
350
   */
351
  readFactChunk_(buffer) {
352
    /** @type {?Object} */
353
    let chunk = this.findChunk('fact');
354
    if (chunk) {
355
      this.head = chunk.chunkData.start;
356
      this.fact.chunkId = chunk.chunkId;
357
      this.fact.chunkSize = chunk.chunkSize;
358
      this.fact.dwSampleLength = this.readUInt32(buffer);
359
    }
360
  }
361
362
  /**
363
   * Read the 'cue ' chunk of a wave file.
364
   * @param {!Uint8Array} buffer The wav file buffer.
365
   * @private
366
   */
367
  readCueChunk_(buffer) {
368
    /** @type {?Object} */
369
    let chunk = this.findChunk('cue ');
370
    if (chunk) {
371
      this.head = chunk.chunkData.start;
372
      this.cue.chunkId = chunk.chunkId;
373
      this.cue.chunkSize = chunk.chunkSize;
374
      this.cue.dwCuePoints = this.readUInt32(buffer);
375
      for (let i = 0; i < this.cue.dwCuePoints; i++) {
376
        this.cue.points.push({
377
          dwName: this.readUInt32(buffer),
378
          dwPosition: this.readUInt32(buffer),
379
          fccChunk: this.readString(buffer, 4),
380
          dwChunkStart: this.readUInt32(buffer),
381
          dwBlockStart: this.readUInt32(buffer),
382
          dwSampleOffset: this.readUInt32(buffer),
383
        });
384
      }
385
    }
386
  }
387
388
  /**
389
   * Read the 'smpl' chunk of a wave file.
390
   * @param {!Uint8Array} buffer The wav file buffer.
391
   * @private
392
   */
393
  readSmplChunk_(buffer) {
394
    /** @type {?Object} */
395
    let chunk = this.findChunk('smpl');
396
    if (chunk) {
397
      this.head = chunk.chunkData.start;
398
      this.smpl.chunkId = chunk.chunkId;
399
      this.smpl.chunkSize = chunk.chunkSize;
400
      this.smpl.dwManufacturer = this.readUInt32(buffer);
401
      this.smpl.dwProduct = this.readUInt32(buffer);
402
      this.smpl.dwSamplePeriod = this.readUInt32(buffer);
403
      this.smpl.dwMIDIUnityNote = this.readUInt32(buffer);
404
      this.smpl.dwMIDIPitchFraction = this.readUInt32(buffer);
405
      this.smpl.dwSMPTEFormat = this.readUInt32(buffer);
406
      this.smpl.dwSMPTEOffset = this.readUInt32(buffer);
407
      this.smpl.dwNumSampleLoops = this.readUInt32(buffer);
408
      this.smpl.dwSamplerData = this.readUInt32(buffer);
409
      for (let i = 0; i < this.smpl.dwNumSampleLoops; i++) {
410
        this.smpl.loops.push({
411
          dwName: this.readUInt32(buffer),
412
          dwType: this.readUInt32(buffer),
413
          dwStart: this.readUInt32(buffer),
414
          dwEnd: this.readUInt32(buffer),
415
          dwFraction: this.readUInt32(buffer),
416
          dwPlayCount: this.readUInt32(buffer),
417
        });
418
      }
419
    }
420
  }
421
422
  /**
423
   * Read the 'data' chunk of a wave file.
424
   * @param {!Uint8Array} buffer The wav file buffer.
425
   * @param {boolean} samples True if the samples should be loaded.
426
   * @throws {Error} If no 'data' chunk is found.
427
   * @private
428
   */
429
  readDataChunk_(buffer, samples) {
430
    /** @type {?Object} */
431
    let chunk = this.findChunk('data');
432
    if (chunk) {
433
      this.data.chunkId = 'data';
434
      this.data.chunkSize = chunk.chunkSize;
435
      if (samples) {
436
        this.data.samples = buffer.slice(
437
          chunk.chunkData.start,
438
          chunk.chunkData.end);
439
      }
440
    } else {
441
      throw Error('Could not find the "data" chunk');
442
    }
443
  }
444
445
  /**
446
   * Read the 'bext' chunk of a wav file.
447
   * @param {!Uint8Array} buffer The wav file buffer.
448
   * @private
449
   */
450
  readBextChunk_(buffer) {
451
    /** @type {?Object} */
452
    let chunk = this.findChunk('bext');
453
    if (chunk) {
454
      this.head = chunk.chunkData.start;
455
      this.bext.chunkId = chunk.chunkId;
456
      this.bext.chunkSize = chunk.chunkSize;
457
      this.bext.description = this.readString(buffer, 256);
458
      this.bext.originator = this.readString(buffer, 32);
459
      this.bext.originatorReference = this.readString(buffer, 32);
460
      this.bext.originationDate = this.readString(buffer, 10);
461
      this.bext.originationTime = this.readString(buffer, 8);
462
      this.bext.timeReference = [
463
        this.readUInt32(buffer),
464
        this.readUInt32(buffer)];
465
      this.bext.version = this.readUInt16(buffer);
466
      this.bext.UMID = this.readString(buffer, 64);
467
      this.bext.loudnessValue = this.readUInt16(buffer);
468
      this.bext.loudnessRange = this.readUInt16(buffer);
469
      this.bext.maxTruePeakLevel = this.readUInt16(buffer);
470
      this.bext.maxMomentaryLoudness = this.readUInt16(buffer);
471
      this.bext.maxShortTermLoudness = this.readUInt16(buffer);
472
      this.bext.reserved = this.readString(buffer, 180);
473
      this.bext.codingHistory = this.readString(
474
        buffer, this.bext.chunkSize - 602);
475
    }
476
  }
477
478
  /**
479
   * Read the 'ds64' chunk of a wave file.
480
   * @param {!Uint8Array} buffer The wav file buffer.
481
   * @throws {Error} If no 'ds64' chunk is found and the file is RF64.
482
   * @private
483
   */
484
  readDs64Chunk_(buffer) {
485
    /** @type {?Object} */
486
    let chunk = this.findChunk('ds64');
487
    if (chunk) {
488
      this.head = chunk.chunkData.start;
489
      this.ds64.chunkId = chunk.chunkId;
490
      this.ds64.chunkSize = chunk.chunkSize;
491
      this.ds64.riffSizeHigh = this.readUInt32(buffer);
492
      this.ds64.riffSizeLow = this.readUInt32(buffer);
493
      this.ds64.dataSizeHigh = this.readUInt32(buffer);
494
      this.ds64.dataSizeLow = this.readUInt32(buffer);
495
      this.ds64.originationTime = this.readUInt32(buffer);
496
      this.ds64.sampleCountHigh = this.readUInt32(buffer);
497
      this.ds64.sampleCountLow = this.readUInt32(buffer);
498
      //if (wav.ds64.chunkSize > 28) {
499
      //  wav.ds64.tableLength = unpack(
500
      //    chunkData.slice(28, 32), uInt32_);
501
      //  wav.ds64.table = chunkData.slice(
502
      //     32, 32 + wav.ds64.tableLength);
503
      //}
504
    } else {
505
      if (this.container == 'RF64') {
506
        throw Error('Could not find the "ds64" chunk');
507
      }
508
    }
509
  }
510
511
  /**
512
   * Read the 'LIST' chunks of a wave file.
513
   * @param {!Uint8Array} buffer The wav file buffer.
514
   * @private
515
   */
516
  readLISTChunk_(buffer) {
517
    /** @type {?Object} */
518
    let listChunks = this.findChunk('LIST', true);
519
    if (listChunks !== null) {
520
      for (let j=0; j < listChunks.length; j++) {
521
        /** @type {!Object} */
522
        let subChunk = listChunks[j];
523
        this.LIST.push({
524
          chunkId: subChunk.chunkId,
525
          chunkSize: subChunk.chunkSize,
526
          format: subChunk.format,
527
          subChunks: []});
528
        for (let x=0; x<subChunk.subChunks.length; x++) {
529
          this.readLISTSubChunks_(subChunk.subChunks[x],
530
            subChunk.format, buffer);
531
        }
532
      }
533
    }
534
  }
535
536
  /**
537
   * Read the sub chunks of a 'LIST' chunk.
538
   * @param {!Object} subChunk The 'LIST' subchunks.
539
   * @param {string} format The 'LIST' format, 'adtl' or 'INFO'.
540
   * @param {!Uint8Array} buffer The wav file buffer.
541
   * @private
542
   */
543
  readLISTSubChunks_(subChunk, format, buffer) {
544
    if (format == 'adtl') {
545
      if (['labl', 'note','ltxt'].indexOf(subChunk.chunkId) > -1) {
546
        this.head = subChunk.chunkData.start;
547
        /** @type {!Object<string, string|number>} */
548
        let item = {
549
          chunkId: subChunk.chunkId,
550
          chunkSize: subChunk.chunkSize,
551
          dwName: this.readUInt32(buffer)
552
        };
553
        if (subChunk.chunkId == 'ltxt') {
554
          item.dwSampleLength = this.readUInt32(buffer);
555
          item.dwPurposeID = this.readUInt32(buffer);
556
          item.dwCountry = this.readUInt16(buffer);
557
          item.dwLanguage = this.readUInt16(buffer);
558
          item.dwDialect = this.readUInt16(buffer);
559
          item.dwCodePage = this.readUInt16(buffer);
560
        }
561
        item.value = this.readZSTR(buffer, this.head);
562
        this.LIST[this.LIST.length - 1].subChunks.push(item);
563
      }
564
    // RIFF INFO tags like ICRD, ISFT, ICMT
565
    } else if(format == 'INFO') {
566
      this.head = subChunk.chunkData.start;
567
      this.LIST[this.LIST.length - 1].subChunks.push({
568
        chunkId: subChunk.chunkId,
569
        chunkSize: subChunk.chunkSize,
570
        value: this.readZSTR(buffer, this.head)
571
      });
572
    }
573
  }
574
575
  /**
576
   * Read the 'junk' chunk of a wave file.
577
   * @param {!Uint8Array} buffer The wav file buffer.
578
   * @private
579
   */
580
  readJunkChunk_(buffer) {
581
    /** @type {?Object} */
582
    let chunk = this.findChunk('junk');
583
    if (chunk) {
584
      this.junk = {
585
        chunkId: chunk.chunkId,
586
        chunkSize: chunk.chunkSize,
587
        chunkData: [].slice.call(buffer.slice(
588
          chunk.chunkData.start,
589
          chunk.chunkData.end))
590
      };
591
    }
592
  }
593
594
  /**
595
   * Read bytes as a ZSTR string.
596
   * @param {!Uint8Array} bytes The bytes.
597
   * @param {number} index the index to start reading.
598
   * @return {string} The string.
599
   * @protected
600
   */
601
  readZSTR(bytes, index=0) {
602
    for (let i = index; i < bytes.length; i++) {
603
      this.head++;
604
      if (bytes[i] === 0) {
605
        break;
606
      }
607
    }
608
    return unpackString(bytes, index, this.head - 1);
609
  }
610
611
  /**
612
   * Read a number from a chunk.
613
   * @param {!Uint8Array} bytes The chunk bytes.
614
   * @return {number} The number.
615
   * @protected
616
   */
617
  readUInt16(bytes) {
618
    /** @type {number} */
619
    let value = unpack(bytes, this.uInt16, this.head);
620
    this.head += 2;
621
    return value;
622
  }
623
}
624